package lk.icta.core.service.impl;

import lk.icta.common.AppConstants;
import lk.icta.common.ELandRuntimeException;
import lk.icta.common.ErrorCodes;
import lk.icta.common.entity.District;
import lk.icta.common.entity.RailwayTrack;
import lk.icta.core.dao.AlienationRailwayDAO;
import lk.icta.core.dao.PaymentDAO;
import lk.icta.core.dao.TaxationDAO;
import lk.icta.core.entity.*;
import lk.icta.core.service.RailwayPaymentManagementService;
import lk.icta.user.dao.UserDAO;
import lk.icta.user.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.concurrent.CountDownLatch;

/**
 * @author Supun Viraj Rathnayaka
 */
public class RailwayPaymentManagementServiceImpl implements RailwayPaymentManagementService {

    private static final Logger logger = LoggerFactory.getLogger(RailwayPaymentManagementServiceImpl.class);
    private static final int THREADS = 5;

    private CountDownLatch doneSignal = new CountDownLatch(0);

    private final PaymentDAO paymentDAO;
    private final AlienationRailwayDAO alienationRailwayDAO;
    private final UserDAO userDAO;
    private final TaxationDAO taxationDAO;
    private final DataSource dataSource;



    public RailwayPaymentManagementServiceImpl(PaymentDAO paymentDAO, AlienationRailwayDAO alienationRailwayDAO, UserDAO userDAO, TaxationDAO taxationDAO, DataSource dataSource) {
        this.paymentDAO = paymentDAO;
        this.alienationRailwayDAO = alienationRailwayDAO;
        this.userDAO = userDAO;
        this.taxationDAO = taxationDAO;
        this.dataSource = dataSource;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void addPayment(Payment payment, long alienationId, User user) {
        try {

            validatePaymentDetail(payment);
            AlienationRailway alienationRailway = alienationRailwayDAO.getAlienationById(alienationId, user);
            if (alienationRailway != null) {

                List<Payment> paymentList = new ArrayList<Payment>();

                if (alienationRailway.getPayments() != null && alienationRailway.getPayments().size() > 0) {
                    paymentList.addAll(alienationRailway.getPayments());
                }

                paymentList.add(payment);
                alienationRailway.setPayments(paymentList);

                if (alienationRailway.getLeaseDetail() != null) {
                    if (payment.getPaymentType() != Payment.PaymentType.CHEQUE.ordinal()) {
                        alienationRailway = updateAlienationData(alienationRailway, payment.getRentalAmount(), payment.getTaxAmount());
                    }

                    //set alienationId for payment
                    payment.setAlienationId(alienationId);
                    paymentDAO.addPayment(payment, user);

                    alienationRailwayDAO.updateAlienation(alienationRailway, user);

                } else {
                    handleELandRuntimeException(user.getUsername() + " Invalid Data ",
                            ErrorCodes.INVALID_DATA);
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
            handleELandRuntimeException(user.getUsername() + " Exception ",
                    ErrorCodes.EXCEPTION_OCCURRED);
        }

    }

    private AlienationRailway updateAlienationData(AlienationRailway alienationRailway, double amount, double tax) {

        LeaseDetail leaseDetail = alienationRailway.getLeaseDetail();
        List<ArrearsHistoryRecord> ahRecords = alienationRailway.getArrearsHistoryRecords();

        // Update paidAmount and arrears data affected by the payment
        leaseDetail.addPayment(amount);
        leaseDetail.addTaxPayment(tax);
        alienationRailway.setLeaseDetail(leaseDetail);

        if (ahRecords != null){
            int cYear = Calendar.getInstance().get(Calendar.YEAR);

            ArrearsHistoryRecord ahr = null;
            int i = 0;
            for(ArrearsHistoryRecord ah : ahRecords){
                if(ah.getYear() == cYear){
                    ahr = ah;
                    ahRecords.get(i).setArrearsAmount(ahRecords.get(i).getArrearsAmount() - (amount+tax));
                }
                i++;
            }

            if(ahr == null){
                ahr = new ArrearsHistoryRecord();
                ahr.setYear(cYear);
                ahr.setActiveRecord(true);
                ahr.setArrearsAmount(0 - (amount+tax));
                ahRecords.add(ahr);
            }
        }else{
            ArrearsHistoryRecord ahRecord = new ArrearsHistoryRecord();
            ahRecord.setYear(Calendar.getInstance().get(Calendar.YEAR));
            ahRecord.setActiveRecord(true);

            ahRecord.setArrearsAmount(0 - (amount+tax));
            ahRecords = new ArrayList<ArrearsHistoryRecord>();
            ahRecords.add(ahRecord);

        }
        alienationRailway.setArrearsHistoryRecords(ahRecords);
        return alienationRailway;

    }

    private int calculateYears(LeaseDetail leaseDetail) {
        /* Note : This implementation might be wrong. Do this using Calendars*/
        Date leaseStartDate = leaseDetail.getLeaseStartingDate();

        int currentDays = (int) ((new Date().getTime() - leaseStartDate.getTime()) / (1000 * 60 * 60 * 24));
        int currentYears = currentDays % 365;


        /*Calendar cal = Calendar.getInstance();
        cal.setTime(leaseStartDate);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.get(Calendar.DAY_OF_MONTH);


        LocalDate leaseDate = LocalDate.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH));
        LocalDate currentDate = LocalDate.now();

        Period betweenDates = Period.between(leaseDate, currentDate);

        currentYears=betweenDates.getYears();*/


        return currentYears;


    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void confirmPayment(long paymentId, User user) {
        Payment payment = paymentDAO.getPaymentById(paymentId, user);
        if (payment != null || payment.getPaymentType() == Payment.PaymentType.CHEQUE.ordinal()) {
                AlienationRailway alienationRailway = alienationRailwayDAO.getAlienationById(payment.getAlienationId(), user);

                alienationRailway = updateAlienationData(alienationRailway, payment.getRentalAmount(), payment.getTaxAmount());
                // Update the Alienation
                alienationRailwayDAO.updateAlienation(alienationRailway, user);
                // Confirm the Payment
                paymentDAO.confirmPayment(payment, user);

        } else {
            handleELandRuntimeException(user.getUsername() + " PERMISSION DENIED ",
                    ErrorCodes.PERMISSION_DENIED);
        }

    }

    @Override
    public List<Map<String, List<Object>>> getTimelyIncomeReportData(List<Map<String, Date>> timeFrames, List<District> districtList, RailwayTrack railwayTrack, String language) {
        List<AlienationRailway> alienations;
        List<Map<String, List<Object>>> l = new ArrayList<Map<String, List<Object>>>();
        Map<String, List<Object>> data = null;
        for(Map<String, Date> timeFrame : timeFrames){
            data = new HashMap();
            for(District d : districtList){
                alienations = alienationRailwayDAO.getAlienationRailwayByTrackAndDistrictSet(d, railwayTrack);
                if(alienations.size() == 0) continue;
                data.put(d.getDistrictNameByLanguage(language), paymentDAO.getPaymentsInTimeFrame(timeFrame, new HashSet<AlienationRailway>(alienations)));
            }
            l.add(data);
        }
        return l;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void triggerValuationUpdateJob() {
        Date today = new Date();
        logger.info("Initiating Scheduled Job to Update Current Lease Rental Amounts. Starting on {}", today);
        User user = userDAO.getUserByUserName(AppConstants.SYSTEM_USER);
        Calendar cal = getCalendar(today);
        int month = cal.get(Calendar.MONTH) + 1;  // Added 1 since months starts from 0
        int day = cal.get(Calendar.DAY_OF_MONTH);
        List<AlienationRailway> alienationRailwayList = alienationRailwayDAO.getAlienationByValuationStartDay(day, month);
        logger.info("{} Lease(s) found to be updated", alienationRailwayList.size());
        double cRental,percentage;
        boolean vOpt;
        Taxation tData = taxationDAO.getPreviousTaxation();
        Calendar vStart;
        int yearDiff;
        for(AlienationRailway ar : alienationRailwayList){
            vStart = getCalendar(ar.getValuation().getLastValuationPeriodStart());
            yearDiff = cal.get(Calendar.YEAR) - vStart.get(Calendar.YEAR);
            if(yearDiff % AppConstants.VALUATION_RENEW_YEARS == 0){
                cRental = ar.getLeaseDetail().getAnnualRental();
                vOpt = ar.getValuation().getValuationOption();
                percentage = 0; // 50%
                if(vOpt){
                    percentage = ar.getValuation().getAddedPercentage()/100d;
                }
                percentage = percentage > 0 ? percentage : 0.5d;
                double newRent = cRental*(1 + percentage);
                ar.getLeaseDetail().setAnnualRental(newRent);

                alienationRailwayDAO.updateAlienation(ar, user);
            }else{
                continue;
            }
        }
        logger.info("Finished Scheduled Job : Update Current Lease Rental Amounts. Finished on {}", new Date());
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void triggerArrearsUpdateJob() {
        Date today = new Date();
        logger.info("Initiating Scheduled Job to Arrears Amounts. Starting on {}", today);
        User user = userDAO.getUserByUserName(AppConstants.SYSTEM_USER);
        Calendar cal = Calendar.getInstance();
        cal.setTime(today);
        cal.add(Calendar.DATE, 1);
        int month = cal.get(Calendar.MONTH) + 1;  // Added 1 since months starts from 0
        int day = cal.get(Calendar.DAY_OF_MONTH);

        List<AlienationRailway> alienationRailwayList = alienationRailwayDAO.getAlienationByStartedDay(day, month);
        logger.info("{} Lease(s) found to be updated", alienationRailwayList.size());
        Taxation tData = taxationDAO.getPreviousTaxation();
        LeaseDetail ld;
        ArrearsHistoryRecord aar;
        double newRent,tax;
        for(AlienationRailway ar : alienationRailwayList){
            // arrears = totalPayable - totalPaid
            // arrearsTax = totalTaxPayable - totalTaxPaid

            newRent = ar.getLeaseDetail().getAnnualRental();
            tax = newRent*tData.getTaxPercentage()/100d;
            ld = ar.getLeaseDetail();

            ld.addPayable(newRent);
            ld.addTaxPayable(tax);

            List<ArrearsHistoryRecord> ahRecords = ar.getArrearsHistoryRecords();

            if (ahRecords != null){
                int cYear = Calendar.getInstance().get(Calendar.YEAR);

                ArrearsHistoryRecord ahr = null;
                int i = 0;
                for(ArrearsHistoryRecord ah : ahRecords){
                    if(ah.getYear() == cYear){
                        ahr = ah;
                        ahRecords.get(i).setArrearsAmount(ahRecords.get(i).getArrearsAmount() + (newRent+tax));
                    }
                    i++;
                }

                if(ahr == null){
                    ahr = new ArrearsHistoryRecord();
                    ahr.setYear(cYear);
                    ahr.setActiveRecord(true);
                    ahr.setArrearsAmount(newRent+tax);
                    ahRecords.add(ahr);
                }
            }else{
                ArrearsHistoryRecord ahRecord = new ArrearsHistoryRecord();
                ahRecord.setYear(Calendar.getInstance().get(Calendar.YEAR));
                ahRecord.setActiveRecord(true);

                ahRecord.setArrearsAmount(newRent+tax);
                ahRecords = new ArrayList<ArrearsHistoryRecord>();
                ahRecords.add(ahRecord);

            }
            ar.setArrearsHistoryRecords(ahRecords);

//            aar = ar.getArrearsHistoryRecord();
//            if(aar == null){
//                aar = new ArrearsHistoryRecord();
//            }
//            aar.addToCurrentYearArrears(newRent);

            ar.setLeaseDetail(ld);
            alienationRailwayDAO.updateAlienation(ar, user);
        }
        logger.info("Finished Scheduled Job : Update Lease Arrears Amounts. Finished on {}", new Date());

    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void triggerAgeAnalysisJob() {
        Connection con = null;
        Statement s = null;
        ResultSet rs = null;
        try {
            con = dataSource.getConnection();
            s = con.createStatement();
            long start = System.currentTimeMillis();

            rs = s.executeQuery("SELECT count(*) FROM ELAND_RAIL.AlienationRailway");
            rs.next();
            long count = rs.getLong(1);

            rs = s.executeQuery("SELECT min(id) FROM ELAND_RAIL.AlienationRailway");
            rs.next();
            long minIdKey = rs.getLong(1);

            rs = s.executeQuery("SELECT max(id) FROM ELAND_RAIL.AlienationRailway");
            rs.next();
            long maxIdKey = rs.getLong(1);

            long block = ((maxIdKey - minIdKey) / THREADS) + 1;


            int threads = THREADS + 1;
            doneSignal = new CountDownLatch(threads);
            logger.info("Using : " + threads + " threads with a block size of : " + block +
                    " to process : " + count + " rows from : " + minIdKey + " to: " + maxIdKey);

            long startIDUkey = minIdKey;
            long endIDUkey = minIdKey;

            Worker w;
            for (int i = 0; i < threads; i++) {
                endIDUkey += block;
                w = new Worker(i, startIDUkey, endIDUkey, doneSignal);
                w.start();
                startIDUkey = endIDUkey + 1;
            }

            try {
                doneSignal.await();
                logger.info("Processed alienation records in : {}s", (System.currentTimeMillis() - start) / 1000);
            } catch (InterruptedException e) {
                logger.info("Interrupted while waiting for worker threads to complete");
            }

            logger.info("Alienation age analysis process started at : " + new Date() + " successfully completed : " +
                    count + " records. Age analysis took: " + (System.currentTimeMillis() - start) / 1000 + " S");

        } catch (SQLException e) {
            logger.error("Unable to establish the connection with data source");
        } finally {

            if(rs != null) {
                try {
                    rs.close();
                    rs = null;
                } catch (SQLException e) {
                    logger.warn("Failed to close the ResultSet");
                }
            }
            if(s != null){
                try {
                    s.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                    logger.error("Exception closing statement .." , e);
                }
            }
            if (con != null) {
                try {
                    con.close();
                    con = null;
                } catch (Exception e) {
                    logger.warn("Failed to close the SQL connection");
                }
            }
        }

    }

    public void validatePaymentDetail(Payment payment) {
        logger.debug("validatePaymentDetail  ");
        if (payment.getReceiptReferenceNo() == null || payment.getRentalAmount() == 0 ||
                payment.getBillingDate() == null || payment.getTaxAmount() == 0) {

            logger.debug("handleELandRuntimeException");
            handleELandRuntimeException("Adding Payment is incomplete " +
                    "Check required field values", ErrorCodes.INVALID_DATA);
        }
    }

    private void handleELandRuntimeException(String message, int code) {
        logger.error(message);
        throw new ELandRuntimeException(message, code);
    }

    private static Calendar getCalendar(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal;
    }


/**
 * Worker Thread Class
 */

private class Worker extends Thread {

    private final int id;
    private final long startId;
    private final long endId;
    private final CountDownLatch doneSignal;

    private Worker(int id, long startId, long endId, CountDownLatch doneSignal) {
        this.id = id;
        this.startId = startId;
        this.endId = endId;
        this.doneSignal = doneSignal;
    }

    @Override
    public void run() {
        logger.info("Worker : " + id + " will process from id: " + startId + " to id: " + endId);

        try {
            long s = startId;
            long e = startId - 1;
            List<AlienationRailway> alienationRailwayList= null;
            int cYear = Calendar.getInstance().get(Calendar.YEAR);
            /**
             * Process takes 200 records per time from start to end range
             * eg: if start = 1000 and end = 2000
             * first attempt take 1000 to 1199 records to process
             * Note: You can change the record amount taken per iteration by changing the "e += 200" line
             */
            while (s <= endId) {
                e += 200;
                if (e > endId) {
                    e = endId;
                }

                alienationRailwayList = alienationRailwayDAO.getAlienationsByIdRnage(s,e);
                User user = userDAO.getUserByUserName(AppConstants.SYSTEM_USER);
                List<ArrearsHistoryRecord> ahRecords = null;
                ArrearsHistoryRecord ahRecord;
                LeaseDetail ld;
                for(AlienationRailway ar : alienationRailwayList){
                    ahRecords = ar.getArrearsHistoryRecords();
                    int i = 0;
                    if(ahRecords != null){
                        for(ArrearsHistoryRecord ah : ahRecords){
                            if(ah.getYear() == cYear){
                                ahRecords.get(i).setActiveRecord(false);
                            }
                            i++;
                        }
                    }else{
                        ahRecords = new ArrayList<ArrearsHistoryRecord>();
                    }
                    ahRecord = new ArrearsHistoryRecord();
                    ahRecord.setYear(cYear+1);
                    ahRecord.setActiveRecord(true);
                    ahRecords.add(ahRecord);
                    ar.setArrearsHistoryRecords(ahRecords);
                    setLandLogComment(ar.getLand(), "Updated Arrears History Record ", user);
                    alienationRailwayDAO.updateAlienation(ar,user);
                }
                s = e + 1;
            }

        } finally {
            doneSignal.countDown();
            logger.info("Worker : {} exiting. Remaining threads : {}", id, doneSignal.getCount());
        }
    }

}

    protected void setLandLogComment(LandRailway land, String approveStr, User user) {
        if (land.getApprovalNote() != null) {
            land.setApprovalNote(land.getApprovalNote() + "\n" + approveStr + " by :" + user.getUsername() + ", Date : " + new Date() + ".");
        } else {
            land.setApprovalNote(approveStr + " by :" + user.getUsername() + ", Date : " + new Date() + ".");
        }
    }
}


